总结
- nginx
- 可以快速搭建一个静态资源服务器。
- 通过简单的配置可以实现反向代理 负载均衡等基础功能,不需要额外开发。
- 底层用 C 进行编写,性能比 JS 强。
- 应用 - 反向代理:为用户降低负担,免记端口号。
- 应用 - rewrite:实现单页面应用访问任意 *.html 都读取根目录 index.html。
- docker
- 环境隔离
- 每个项目运行在自己的容器中,不会相互影响
- 环境配置过程简单
- 在迁移服务器的时候极为方便
- 多项目服务器上,自由和灵活度大大提升
- 主动设置
Content-Length
可以减少传输内容,提高性能。节省20个字节左右,chunck 越多,节省越多。
1. 为什么要从简单的静态资源服务器,开始学习docker
从最简单的 Node 静态资源服务器中,可以了解 Node 静态资源服务器的实现和弊端。从而真实地感知到 nginx 和 docker 具有什么优势,解决了哪些问题,
2. 静态资源服务器
首先需要了解概念,什么是静态资源服务器?
静态资源服务器:不会被服务器的动态运行所改变或者生成的文件———称为静态资源,而可以对客户端的请求进行响应并返回这些静态资源的服务,称为静态资源服务器。
动态资源服务器:与静态相对,动态资源需要根据客户端的请求,并动态生成对应的资源,将这些资源响应给客户端的服务称为动态资源服务器。
两者的区别:资源一开始便存在,不需要查数据库也不需要程序处理,满足为静态资源,否则动态。
3. Node 静态资源服务器
Node 因为 JavaScript 的原因和前端最为贴合,所以优先考虑学习成本最低的 Node 来搭建一个静态资源服务器。
实现一个响应 HTML 文件的服务器
// server-fs.js
const http = require('node:http')
const fs = require('node:fs')
// 上段代码这里是一段字符串,而这里通过读取文件获取内容
const html = fs.readFileSync('./index.html')
const server = http.createServer((req, res) => res.end(html))
server.listen(3000, () => {
console.log('Listening 3000')
})
步骤:
- 引入内置模块(builtinModule)
node:fs
。 - 使用 fs 模块的
fs.readFileSync
方法读取静态资源。 - 引入内置模块
node:http
。 - 使用 http 模块的
http.createServer
方法创建服务。并指定使用res.end(html)
设置响应体为上方的静态资源。 - 使用服务的
server.listen
监听指定端口。
node:
前缀见官方文档 core-modules,需升级 node 版本号至 v14.18.0 及以上
执行 node server-fs.js
一个简单的 node 静态服务器就启动了。
通过 curl -vvv localhost:3000
可获得报文信息进行验证,效果如下,可以看到已经可以响应 HTML 文件了。
4. 为什么需要专业的静态资源服务器
既然能够利用 Node 写静态服务器,为什么要需要专业的静态资源服务器。因为手写服务器有以下缺点.
4.1 开发效率低
开发效率低,一个静态资源服务器要实现的基础功能有很多。例如 Rewrite、Redirect 都需要重新开发。
Rewrite 场景:
- 单页应用的所有
*.html
均为读取根目录 index.html。 - vuepress 等静态网站生成器将会有 .html 后缀,此时可通过 Rewrite 去除后缀。比如,将 /hello 重写到 /hello.html。(去除后缀名的功能也叫 cleanUrls,vercel)
4.2 性能低
// 读取整个文件再返回,性能低
const html = fs.readFileSync('./index.html')
const server = http.createServer((req, res) => res.end(html))
// 通过 fs.createReadStream,提升该静态服务器的性能
const server = http.createServer((req, res) => {
// 此处需要手动处理下 Content-Length
fs.createReadStream('./index.html').pipe(res)
})
进行优化:fs.readFileSync
:需要将文件内容整块读入内存中,再进行发送。fs.createStream
:基于流读多少发多少,分块发送,响应更快。存在缓冲,从而避免短时间内大量数据占用内存导致问题。替换 fs.readFileSync 方法。
即使可以有这些优化,但 Javascript 性能有限,使用 nginx 作为静态资源服务器拥有更高的性能。
5.初步认识 nginx、docker
在上面我们可以启动一个静态服务,并且监听指定端口。但是如果一个服务器有许多项目,不同项目的服务监听不同的端口,用户访问不同项目就需要敲入不同端口号。
用户需要记住端口号,这个负担很沉重。通过 nginx 进行反向代理后,可以通过二级域名或者路由,直接访问不同的项目。而且 nginx 提供了许多静态服务器需要的基础功能,大大提高了开发效率。
不同的项目需要不同的环境,可能是不同语言,如 Java、Node、GO编写的,甚至同个语言下的不同版本如 Node。这些环境的配置相当繁琐,在系统迁移的时候也十分麻烦,docker 可以启动一个容器和宿主环境隔离开来,每个项目运行在自己的容器中,自由和灵活度大大提升。
6. 扩展
6.1 为什么基于 stream 的静态服务器拥有更高的性能?
fs.readFile
:一次性读取,需要将文件内容完整读入内存中,才能进行传输。占用内存大,且读取过程中的时间浪费了。
fs.createStream
:基于流 stream 的静态服务器,将文件分成多个数据块进行读取,已读的数据可以立即进行传输,不用再等待整个文件读取完,响应更快。存在缓冲,避免占用太大内存。
6.2 为什么基于 stream 后,serve 等静态资源服务器仍然会计算 Content-Length?
先说结论
目的:减少传输内容,提高性能。
设置Content-Length
能减少传输内容,节省20个字节左右,chunck 越多,节省越多。实现:
fs.stat
方法可获取文件的信息,其中的size
属性为文件大小,可以将其设置在响应头Content-Length
,后续使用createReadableStream
创建一个可读流并传输给客户端。jsconst http = require('http') const fsp = require('fs/promises') const fs = require('fs') const server = http.createServer(async (req, res) => { const stat = await fsp.stat('./index.html') res.setHeader('Content-Length', stat.size) fs.createReadStream('./index.html').pipe(res) }) const port = 8888 server.listen(port, () => { console.log(`Listening ${port}`) })
为什么要在响应头返回 Content-Length:
不设置
Content-Length
:http 响应报文的 会有Transfer-Encoding: chunked
响应头。该响应头告知浏览器,响应的正文不是一次性传输,而是分成一个个快(chunk)。
每个 chunk :- length(长度) + \r\n(换行标志)
- chunk data(数据包) + \r\n(换行标志)
连续两个换行代表结束:按照规定,结束的时候发送结束分块,长度头值为0,分块数据没有内容。看上去便是连续两个换行。
设置了
Content-Length
:stream 还是以分块的形式传输,但是没有 长度头,分块数据也没有回车换行 。当客户端读取 Content-length 的值大小的资源时,表示数据接收完毕。
理论图
实际测试图
设置了 Content-Length
- 每个块将省去 长度头(2字节长度值,2字节回车换行) 和 数据后的回车换行(2字节),总共 6 个字节。
- 省去最后的结束分块和空行(1字节长度值,4字节回车换行),总共 5 字节。
- 省去
Transfer-Encoding: chunked.
响应头:总共 28 字节 - 多了
Content-Length:
和 长度值字节: 总共 16 字节 + 长度值字节
验证加了Content-Length,是否一定减少传输内容。
做个简单的计算:假设分成 1 块,因为分块越多(每块减6字节),传输内容减少越多,我们以影响最小的计算。
- 6 - 5 - 28 + 16 + 长度值字节
= 长度值字节 - 23
如果结果小于0,则证明传输内容减少
即 长度值字节 < 23 字节 传输内容减少
长度值代表这个包的大小,这个值完完全全够不到 23 字节,23 字节可以表示一个天文数字了。
所以设置 Content-Length
就能够减少传输内容。
使用curl --trace log.txt localhost:8888 可以转储所有传入和传出的数据到文件,包括描述信息。
为了提高效率 更好 还是使用压缩 nginx gzip on 压缩 text/html 文本
6.3 继续完善静态服务器,使其作为一个命令行工具,支持指定端口号、读取目录、404、stream (甚至 trailingSlash、cleanUrls、rewrite、redirect 等)。可参考 serve-handler。
6.4 什么是 rewrite 和 redirect?
在客户端请求的资源不在本服务器,或资源不匹配非客户请求路径 的情况下
- rewrite:内部重定向,服务器去目标服务器获取资源,获取到资源后响应给客户端。客户端只需要发送 1 个请求就可以获取到资源。
- redirect:重定向,服务器通过 301/302 状态码,将资源地址存放在Location 响应头告知客户端,客户端再发送请求去获取资源。客户端需要发送 2 个请求才可以获取到资源。
提问
[x] HTTP响应,基于node的stream实现,如何返回正确的content-length?
因为有实体文件,可以读取文件的信息,fs.stat
获取文件大小。[x] 分别提供content-length和Transfer-Encoding: chunked,我们如何知道一个HTTP响应接收完毕?
content-length 接收数据达到指定长度
Transfer-Encoding: chunked 连续两个换行。
[x] 服务器想将请求重定向到另一个地址,是怎样告诉浏览器的
状态码 301,设置 Location 作为重定向地址。